Skip to content

perf(build): replace specs glob with fs reads#2959

Open
atharvadeosthale wants to merge 5 commits into
mainfrom
website-optimize
Open

perf(build): replace specs glob with fs reads#2959
atharvadeosthale wants to merge 5 commits into
mainfrom
website-optimize

Conversation

@atharvadeosthale
Copy link
Copy Markdown
Member

Summary

The 11 import.meta.glob calls in specs.ts (one per Appwrite version) expanded into ~58k lazy import() statements, producing 49,123 server chunks (500 MB) and pushing peak build RSS to 7 GB. The matched .md and .json files are static text — there's no reason to route them through Vite's module graph.

This PR resolves @appwrite.io/specs at runtime via createRequire and reads examples + OpenAPI specs with fs.readFile. The package moves from devDependencies to dependencies so it survives bun install --production and ships to the final image. vite-plugin-dynamic-import is removed (no longer needed).

Build impact

metric before after delta
wall time 192s 113s −41%
peak RSS 7.0 GB 5.6 GB −21%
SSR modules 50,269 4,340 −91%
server chunks 49,123 3,194 −93%
build/server/ size 501 MB 80 MB −84%
final image (build/server + specs in node_modules) 501 MB 376 MB −125 MB

Test plan

  • Build runs clean
  • /docs/references/cloud/server-nodejs/databases renders code examples (Node.js)
  • /docs/references/1.5.x/server-python/storage renders (Python, older version)
  • /docs/references/1.7.x/client-android-kotlin/account renders (exercises the Android special path in loadExample)

The 11 import.meta.glob calls in specs.ts expanded into 58k lazy import
statements, producing 49,123 server chunks (500 MB) and pushing peak
build RSS to 7 GB. The .md and .json files were being treated as JS
modules with full transform/sourcemap/chunk overhead, when they are
just static text.

Resolve @appwrite.io/specs at runtime via createRequire and load
examples + OpenAPI specs with fs.readFile. Move the package from
devDependencies to dependencies so it survives bun install --production
and ships to the final image.

Build wall: 192s -> 113s. Peak RSS: 7.0 GB -> 5.6 GB. Server chunks:
49,123 -> 3,194. Final image net ~125 MB smaller (build/server drops
421 MB, node_modules adds 296 MB).

Drops vite-plugin-dynamic-import which is no longer needed.
@appwrite
Copy link
Copy Markdown

appwrite Bot commented May 4, 2026

Appwrite Website

Project ID: 69d7efb00023389e8d27

Sites (1)
Site Status Logs Preview QR
 website
69d7f2670014e24571ca
Building Building View Logs Preview URL QR Code

Website (appwrite/website)

Project ID: 684969cb000a2f6c0a02

Sites (1)
Site Status Logs Preview QR
 website
68496a17000f03d62013
Queued Queued View Logs Preview URL QR Code


Tip

Schedule functions to run as often as every minute with cron expressions

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 4, 2026

Greptile Summary

This PR eliminates the 11 import.meta.glob calls that expanded ~58k lazy import() statements into Vite's module graph, replacing them with direct fs.readFile calls at SSR request time. @appwrite.io/specs is moved to dependencies so it survives bun install --production, and scripts/build.js copies the specs data into build/_specs_data for environments where node_modules is absent at runtime.

  • locateSpecsRoot() tries the installed package first and falls back to a set of _specs_data candidates relative to the SSR bundle, covering both Docker-prod and Appwrite-Sites deployments.
  • getApi now caches parsed JSON in a module-level Map and validates version/platform against explicit allowlists before any filesystem path construction.
  • Example loading is fanned out with Promise.all so all per-method reads for a service page are issued concurrently.

Confidence Score: 4/5

The build optimisation is sound and the key correctness concerns (version validation, path traversal, parallel example reads, JSON caching) are all addressed in the new code.

The rewrite is structurally correct and the test plan is comprehensive. The open item from a prior review thread (cache key should be version|mode not version|platform) is still unaddressed, so on a busy SSR server the same multi-MB JSON document can accumulate one copy per server-side platform variant. No new blocking correctness issues were found in this pass.

src/routes/docs/references/[version]/[platform]/[service]/specs.ts — specifically the apiCache key at line 414 and the locateSpecsRoot fallback candidate paths, which assume a fixed SvelteKit output depth.

Important Files Changed

Filename Overview
src/routes/docs/references/[version]/[platform]/[service]/specs.ts Core rewrite: replaces 11 import.meta.glob calls with direct fs.readFile; adds input validation guards, an in-process apiCache, and a fallback locateSpecsRoot for environments without node_modules. Cache key uses full platform string rather than the collapsed mode literal, causing redundant entries for same-file platforms (noted in prior thread).
scripts/build.js New post-build copy step that resolves @appwrite.io/specs via createRequire and copies specs/examples into build/_specs_data. Uses a regex string replacement to compute projectRoot, which is slightly fragile compared to path.resolve.
package.json Moves @appwrite.io/specs from devDependencies to dependencies so it survives bun install --production; removes vite-plugin-dynamic-import.
vite.config.ts Removes the dynamicImport plugin that was only needed to handle the specs globs — straightforward cleanup.
bun.lock Lock file updated to reflect the dependency move and vite-plugin-dynamic-import removal.

Reviews (5): Last reviewed commit: "Merge branch 'main' into website-optimiz..." | Re-trigger Greptile

Comment thread src/routes/docs/references/[version]/[platform]/[service]/specs.ts
Comment thread src/routes/docs/references/[version]/[platform]/[service]/specs.ts Outdated
Comment thread src/routes/docs/references/[version]/[platform]/[service]/specs.ts Outdated
…RL params

- Cache parsed OpenAPI documents in a module-scope Map keyed by
  version|platform. Restores the implicit caching the old import.meta.glob
  path got from Node's ESM module cache, avoids re-parsing multi-MB JSON
  on every request.

- Replace the serial loadExample await loop with Promise.all over the
  prepared method list. Cuts I/O latency on services with many endpoints.

- Validate version and platform against allowlists from references.ts
  before any path.join. URL segments shouldn't reach a filesystem read
  unguarded; a lone .. segment could escape examples/ otherwise.
Comment thread src/routes/docs/references/[version]/[platform]/[service]/specs.ts
getApi only interpolates a derived literal mode (server/client/console)
into the JSON path, never platform itself, so the platform allowlist
there was rejecting valid internal callers (model-markdown.ts and
models/[model]/+page.server.ts hardcode 'console-web' which isn't in
the public Platform enum).

Move the guard to getService where the raw platform value is actually
interpolated into examples/${version}/${platform}/... — that's the
real filesystem boundary. URL paths reaching getService still go
through the [platform] route segment whose values come from the
Platform enum, so the check stays correct there.

Fixes 69 failing redirect tests for /docs/references/cloud/models/*.
Comment on lines +414 to 422
const cacheKey = `${version}|${platform}`;
const cached = apiCache.get(cacheKey);
if (cached) {
return cached;
}

const isClient = platform.startsWith('client-');
const mode = platform.startsWith('server-') ? 'server' : isClient ? 'client' : 'console';
const filename = `open-api3-${version}-${mode}.json`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The apiCache key includes the full platform string (e.g. server-nodejs, server-python), but the JSON file actually read is determined by mode — one of three fixed literals (server, client, console). Every server-side platform for the same version maps to the same open-api3-${version}-server.json file, so each distinct platform string creates a separate cache entry that parses and stores an identical multi-MB document. On a live SSR server where different users request server-nodejs, server-python, server-php, etc. concurrently, the same JSON is parsed and allocated once per platform variant — partially undoing the memory benefit of the cache. The key should be ${version}|${mode} so all server platforms share one entry.

Suggested change
const cacheKey = `${version}|${platform}`;
const cached = apiCache.get(cacheKey);
if (cached) {
return cached;
}
const isClient = platform.startsWith('client-');
const mode = platform.startsWith('server-') ? 'server' : isClient ? 'client' : 'console';
const filename = `open-api3-${version}-${mode}.json`;
const isClient = platform.startsWith('client-');
const mode = platform.startsWith('server-') ? 'server' : isClient ? 'client' : 'console';
const cacheKey = `${version}|${mode}`;
const cached = apiCache.get(cacheKey);
if (cached) {
return cached;
}
const filename = `open-api3-${version}-${mode}.json`;

# Conflicts:
#	bun.lock
#	package.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants